State Machine 패턴
1. 개요 [편집]
2. 동기 [편집]
스마트 컨트랙트는 배포 후 코드 수정이 불가능하다. 그런데 경매, 에스크로, 거버넌스 등 대부분 온체인 비즈니스 로직은 여러 단계를 순서대로 거치는 생애주기를 가진다.
경매에서 정산이 끝난 뒤 입찰을 넣거나 에스크로에서 입금 전에 물건 수령을 확인하는 것은 허용되면 안 된다.
이러한 "잘못된 시점에 잘못된 함수가 호출되는" 버그는 전통 소프트웨어에서는 패치로 해결할 수 있지만 불변(immutable)한 스마트 컨트랙트에서는 자금 손실로 직결된다.[3]
State Machine 패턴은 이 문제를 해결하기 위해 설계되었다.
이 패턴이 해결하는 핵심 문제는 다음과 같다.
경매에서 정산이 끝난 뒤 입찰을 넣거나 에스크로에서 입금 전에 물건 수령을 확인하는 것은 허용되면 안 된다.
이러한 "잘못된 시점에 잘못된 함수가 호출되는" 버그는 전통 소프트웨어에서는 패치로 해결할 수 있지만 불변(immutable)한 스마트 컨트랙트에서는 자금 손실로 직결된다.[3]
State Machine 패턴은 이 문제를 해결하기 위해 설계되었다.
이 패턴이 해결하는 핵심 문제는 다음과 같다.
- 단계 간 순서 강제 - 정의되지 않은 경로로의 상태 전이를 구조적으로 차단한다.
- 함수 호출 시점 제어 - 각 함수가 실행 가능한 상태를 컴파일 타임에 선언하여, 잘못된 시점의 호출을 런타임에서 revert한다.
- 시간 조건 자동 적용 - 블록체인에 cron이 없는 환경에서 시간 경과에 따른 단계 전환을 트랜잭션 호출 시점에 자동 반영한다.
- 감사 용이성 - 상태와 전이를 다이어그램으로 시각화할 수 있어 코드 리뷰와 보안 감사가 용이해진다.[1]
3. 구성 요소 [편집]
3.1. 상태 정의 - enum [편집]
enum Stage {
Created,
Bidding,
Revealing,
Settled
}
Stage public currentStage = Stage.Created;
내부적으로
uint8로 저장된다. 정의되지 않은 값으로의 캐스팅은 revert된다. (Solidity 0.8+)[4]3.2. 상태 검증 - modifier [편집]
modifier atStage(Stage expected) {
require(currentStage == expected, "Invalid stage");
_;
}
함수에
atStage(Stage.Bidding)을 붙이면 Bidding 상태에서만 실행된다.3.3. 전이 함수 [편집]
선형 전이 - 순서대로만 진행:
명시적 전이 - 허용된 경로만:
function nextStage() internal {
currentStage = Stage(uint(currentStage) + 1);
}
명시적 전이 - 허용된 경로만:
function transitionTo(Stage next) internal {
require(allowedTransitions[currentStage][next]);
currentStage = next;
}
3.4. 시간 기반 전이 - timedTransition [편집]
블록체인에는 cron이 없으므로 다음 트랜잭션 호출 시 시간 경과를 검사한다.[5]
modifier timedTransition() {
if (currentStage == Stage.Bidding
&& block.timestamp >= createdAt + BIDDING_DURATION)
currentStage = Stage.Revealing;
if (currentStage == Stage.Revealing
&& block.timestamp >= createdAt + BIDDING_DURATION + REVEAL_DURATION)
currentStage = Stage.Settled;
_;
}
주의:
if를 연속 사용한다(else if 아님). 오랜 기간 트랜잭션이 없었을 때 한 번의 호출로 연쇄 전이가 가능해야 하기 때문이다.3.5. 이벤트 [편집]
event StageChanged(Stage indexed from, Stage indexed to, address indexed triggeredBy);
모든 상태 전이에서 이벤트를 발행하여 오프체인 추적을 가능하게 한다.
4. 구현 패턴 [편집]
4.1. 선형 전이 [편집]
상태가 순차적으로만 진행되는 경우.
경매, ICO, 투표, 타임락에 적합하다.
nextStage()로 uint(currentStage) + 1 계산.경매, ICO, 투표, 타임락에 적합하다.
4.2. 비선형 전이 - 전이 테이블 [편집]
분기·복귀가 있는 워크플로우에서 사용한다. 허용된 전이를 mapping으로 정의한다.[1]
mapping(Stage => mapping(Stage => bool)) private allowedTransitions;
constructor() {
allowedTransitions[Stage.AwaitingPayment][Stage.Funded] = true;
allowedTransitions[Stage.Funded][Stage.Delivered] = true;
allowedTransitions[Stage.Funded][Stage.Disputed] = true;
allowedTransitions[Stage.Disputed][Stage.Delivered] = true; // resolve
allowedTransitions[Stage.Disputed][Stage.Cancelled] = true; // cancel
}
4.3. 비트마스크 최적화 [편집]
이중 mapping은 검증당 SLOAD 2회가 필요하다. 비트마스크를 사용하면 SLOAD 1회 + 비트 연산으로 줄일 수 있다.[6]
mapping(Stage => uint8) private transitionMap;
// AwaitingPayment -> Funded(bit 1) 허용
transitionMap[Stage.AwaitingPayment] = 1 << uint8(Stage.Funded);
function transitionTo(Stage next) internal {
require(transitionMap[currentStage] & (1 << uint8(next)) != 0);
currentStage = next;
}
방식 | 가스 비용 (검증 1회) | 적합한 경우 |
이중 mapping | ~4,200 gas (SLOAD × 2) | 가독성 우선 |
비트마스크 | ~2,100 gas (SLOAD × 1) | 호출 빈번, 가스 최적화 필요 |
선형 nextStage() | ~200 gas (연산만) | 순차 전이만 있을 때 |
5. 보안 고려사항 [편집]
5.1. Reentrancy (재진입 공격) [편집]
5.2. block.timestamp 조작 [편집]
시간 기반 전이(
timedTransition)는 block.timestamp에 의존한다. 블록 제안자는 이 값을 실제 시간 대비 약 15초 범위 내에서 조작할 수 있다.[8] 따라서 각 단계의 최소 길이를 수 분 이상으로 설정해야 한다.5.3. Front-running [편집]
상태 전이를 발생시키는 트랜잭션이 mempool에 공개되면, 공격자가 이를 관찰하고 더 높은 가스비로 자신의 트랜잭션을 먼저 포함시킬 수 있다.[9] 대응으로는 commit-reveal 스킴이나 전이 전 최소 지연 시간을 두는 방법이 있다.
6. 활용 사례 [편집]
분야 | 예시 | 상태 흐름 |
DeFi 거버넌스 | Aave, Compound Governor | Pending → Active → Succeeded/Defeated → Queued → Executed |
NFT 민팅 | ERC-721 드롭 | Inactive → Whitelist → Public → SoldOut → Revealed |
에스크로 | 결제 보호 | AwaitingPayment → Funded → Delivered → Complete |
공급망 | 제품 추적 | Manufactured → Shipped → InTransit → Delivered → Verified |
스테이킹 | PoS 프로토콜 | Unlocked → Staked → Cooldown → Withdrawable |
7. 관련 패턴 [편집]
- Access Restriction 패턴 - 역할 기반 접근 제어. State Machine과 결합하여 "누가 + 언제" 조건을 완성한다.
- Checks-Effects-Interactions 패턴 - 상태 변경 후 외부 호출. State Machine의 전이 보안에 필수적이다.
- Pull over Push 패턴 -
withdraw()패턴. 상태 전이와 자금 인출을 분리하여 안전성을 높인다.
8. 관련 문서 [편집]
[1] 1.1 1.2 1.3 Wohrer, Maximilian; Zdun, Uwe (2018). Smart Contracts: Security Patterns in the Ethereum Ecosystem and Solidity. 2018 International Workshop on Blockchain Oriented Software Engineering (IWBOSE). IEEE. pp. 2–8.[2] Hopcroft, John E.; Ullman, Jeffrey D. (1979). Introduction to Automata Theory, Languages, and Computation. Addison-Wesley. ISBN 0-201-02988-X.[3] 3.1 3.2 Atzei, Nicola; Bartoletti, Massimo; Cimoli, Tiziana (2017). A Survey of Attacks on Ethereum Smart Contracts (SoK). Principles of Security and Trust. Springer. pp. 164–186.[4] Solidity Documentation. Enums. docs.soliditylang.org.[5] Solidity Documentation. Common Patterns: State Machine. docs.soliditylang.org.[6] Ethereum Yellow Paper. Ethereum: A Secure Decentralised Generalised Transaction Ledger. Appendix G. Fee Schedule.[7] ConsenSys Diligence. Reentrancy. Smart Contract Best Practices.[8] ConsenSys Diligence. Timestamp Dependence. Smart Contract Best Practices.[9] Daian, Philip et al. (2020). Flash Boys 2.0. 2020 IEEE Symposium on Security and Privacy. pp. 910–927.